/**
 * Copyright (c) 2010-2011 Pierre Labatut
 *
 * This file is part of Space-Chronicles project.
 *
 * Space-Chronicles is free software: you can redistribute it and/or modify it
 * under the terms of its license which may be found in the accompanying
 * LICENSE file or <http://code.google.com/p/space-chronicles/>.
 */
package space.gl;

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.media.opengl.GL2;
import javax.media.opengl.GL3bc;
import jinngine.math.Matrix4;
import jinngine.math.Vector3;
import space.math.Vector3f;

/**
 *
 * @author Pierre
 */
public class Program {

    public final int programHandle;
    public final Map<String, Integer> uniformLocation = new HashMap<String, Integer>();

    public Program(GL2 gl, Shader... shaders) {
        this(gl, Arrays.asList(shaders));
    }

    public Program(GL2 gl, final Iterable<Shader> shaders) {
        if (gl == null) {
            throw new IllegalArgumentException();
        }
        if (shaders == null) {
            throw new IllegalArgumentException();
        }
        programHandle = gl.glCreateProgram();
        for (Shader shader : shaders) {
            gl.glAttachShader(programHandle, shader.shaderHandle);
        }
        gl.glLinkProgram(programHandle);

        //Check compilation status
        if (glGetProgrami(gl, GL2.GL_LINK_STATUS) != GL2.GL_TRUE) {
            throw new RuntimeException(
                    "Program link failed\n" + getInfoLog(gl));
        }

        final int MAX_LENGTH = 255;
        // Now enumerate every uniform location to feed a Strign to Location mapping
        final int maxUniform = glGetProgrami(gl, gl.GL_ACTIVE_UNIFORMS);
        for (int location = 0; location < maxUniform; location++) {
            final IntBuffer length = IntBuffer.wrap(new int[1]);
            final IntBuffer size = IntBuffer.wrap(new int[1]);
            final IntBuffer type = IntBuffer.wrap(new int[1]);
            final ByteBuffer name = ByteBuffer.allocate(MAX_LENGTH);
            gl.glGetActiveUniform(programHandle, location, MAX_LENGTH, length, size, type, name);
            final StringBuffer uname = new StringBuffer();
            do {
                byte c = name.get();
                if (c == 0) {
                    break;
                }
                uname.appendCodePoint(c);
            } while (true);
            if (uniformLocation.put(uname.toString(), location) != null) {
                throw new RuntimeException("Uniform name '" + uname + "' has already be found");
            }
        }


    }

    /**
     * Returns a single integer state of this program
     * @param gl the current gl context
     * @param pname name of the state to fetch
     * @return single integer shader state
     */
    private int glGetProgrami(GL2 gl, int pname) {
        if (gl == null) {
            throw new IllegalArgumentException();
        }

        int[] result = new int[1];
        gl.glGetProgramiv(programHandle, pname, result, 0);
        return result[0];
    }

    /**
     * Returns the log associated with the last compilation of this program
     * @param gl the current gl context
     * @return the complete infolog or null if empty
     */
    private String getInfoLog(GL2 gl) {
        if (gl == null) {
            throw new IllegalArgumentException();
        }

        int infoLength = glGetProgrami(gl, GL2.GL_INFO_LOG_LENGTH);
        if (infoLength <= 0) {
            return null;
        }
        int[] charswritten = new int[1];
        byte[] messageBytes = new byte[infoLength];
        gl.glGetProgramInfoLog(programHandle, infoLength, charswritten, 0,
                messageBytes, 0);
        try {
            //Trim is called to remove the terminal null character
            final String message = new String(messageBytes, "US-ASCII").trim();
            return message.isEmpty() ? null : message;
        } catch (UnsupportedEncodingException ex) {
            throw new RuntimeException(ex);
        }
    }

    public void startUsingProgram(GL3bc gl) {
        gl.glUseProgram(programHandle);
    }

    public void stopUsingProgram(GL3bc gl) {
        gl.glUseProgram(0);
    }

    /**
     * TODO: cache the result
     * @param gl
     * @param uniformName
     * @return
     */
    public void applyUniform4fv(final GL3bc gl, final String uniformName, final Matrix4 matrix) {
        final Integer location = uniformLocation.get(uniformName);
        if (location != null) {
            gl.glUniformMatrix4fv(location, 1, false, matrix.toFloatArray(), 0);
        }
    }

    public void applyUniform1i(final GL3bc gl, final String uniformName, final int i) {
        final Integer location = uniformLocation.get(uniformName);
        if (location != null) {
            gl.glUniform1i(location, i);
        }

    }

    public void applyUniform1f(final GL3bc gl, final String uniformName, final float f) {
        final Integer location = uniformLocation.get(uniformName);
        if (location != null) {
            gl.glUniform1f(location, f);
        }
    }

    public void applyUniform3f(final GL3bc gl, final String uniformName, Vector3f v) {
        final Integer location = uniformLocation.get(uniformName);
        if (location != null) {
            gl.glUniform3f(location, v.x, v.y, v.z);
        }
    }

    public void applyUniform3f(final GL3bc gl, final String uniformName, float x, float y, float z) {
        final Integer location = uniformLocation.get(uniformName);
        if (location != null) {
            gl.glUniform3f(location, x, y, z);
        }
    }

    public void applyUniform3f(final GL3bc gl, final String uniformName, Vector3 v) {
        final Integer location = uniformLocation.get(uniformName);
        if (location != null) {
            gl.glUniform3f(location, (float) v.x, (float) v.y, (float) v.z);
        }
    }

    int getAttribLocation(final GL3bc gl, final String locationsName) {
        return gl.glGetAttribLocation(programHandle, locationsName);
    }
}
